// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_category_axis.dart';
import 'package:charts_grasp/src/chart/axis/multi_level_labels.dart';
import 'package:charts_grasp/src/chart/base/chart_base.dart';
import 'package:charts_grasp/src/chart/chart_series/series.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/rendering_details.dart';
import 'package:charts_grasp/src/common/utils/helper.dart';
import 'package:flutter/material.dart';

abstract class CustomizeAxisElements {
  Color? getAxisLineColor(ChartAxis axis);

  Color? getAxisMajorTickColor(ChartAxis axis, int majorTickIndex);

  Color? getAxisMinorTickColor(ChartAxis axis, int majorTickIndex, int minorTickIndex);

  Color? getAxisMajorGridColor(ChartAxis axis, int majorGridIndex);

  Color? getAxisMinorGridColor(ChartAxis axis, int majorGridIndex, int minorGridIndex);

  double getAxisLineWidth(ChartAxis axis);

  double getAxisMajorTickWidth(ChartAxis axis, int majorTickIndex);

  double getAxisMinorTickWidth(ChartAxis axis, int majorTickIndex, int minorTickIndex);

  double getAxisMajorGridWidth(ChartAxis axis, int majorGridIndex);

  double getAxisMinorGridWidth(ChartAxis axis, int majorGridIndex, int minorGridIndex);

  String getAxisLabel(ChartAxis axis, String text, int labelIndex);

  TextStyle getAxisLabelStyle(ChartAxis axis, String text, int labelIndex);

  int getAxisLabelAngle(ChartAxisRenderer axisRenderer, String text, int labelIndex);

  void drawHorizontalAxesLine(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawVerticalAxesLine(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawHorizontalAxesTickLines(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawVerticalAxesTickLines(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawHorizontalAxesMajorGridLines(Canvas canvas, Offset point, ChartAxisRenderer axisRenderer,
      MajorGridLines grids, int index, CgCartesianChart chart,);

  void drawVerticalAxesMajorGridLines(Canvas canvas, Offset point, ChartAxisRenderer axisRenderer, MajorGridLines grids,
      int index, CgCartesianChart chart,);

  void drawHorizontalAxesMinorLines(Canvas canvas, ChartAxisRenderer axisRenderer, double tempInterval, Rect rect,
      num nextValue, int index, CgCartesianChart chart,);

  void drawVerticalAxesMinorTickLines(
      Canvas canvas, ChartAxisRenderer axisRenderer, num tempInterval, Rect rect, int index, CgCartesianChart chart,);

  void drawHorizontalAxesLabels(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawVerticalAxesLabels(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawHorizontalAxesTitle(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);

  void drawVerticalAxesTitle(Canvas canvas, ChartAxisRenderer axisRenderer, CgCartesianChart chart);
}

// ignore: must_be_immutable
class CartesianAxisWidget extends StatefulWidget {
  CartesianAxisWidget({
    super.key,
    required this.stateProperties,
    required this.renderType,
    required this.dataLabelTemplateNotifier,
  });

  final CartesianStateProperties stateProperties;

  final String renderType;

  late _CartesianAxisWidgetState state;

  final ValueNotifier<int> dataLabelTemplateNotifier;

  @override
  State<StatefulWidget> createState() => _CartesianAxisWidgetState();
}

class _CartesianAxisWidgetState extends State<CartesianAxisWidget> with SingleTickerProviderStateMixin {
  late List<AnimationController> animationControllersList;

  late AnimationController animationController;

  late ValueNotifier<int> axisRepaintNotifier;

  @override
  void initState() {
    axisRepaintNotifier = ValueNotifier<int>(0);
    animationController = AnimationController(vsync: this)..addListener(_repaintAxisElements);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    widget.state = this;
    animationController.duration = const Duration(milliseconds: 1000);
    final Animation<double> axisAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
      parent: animationController,
      curve: const Interval(0.1, 0.9, curve: Curves.decelerate),
    ),);
    animationController.forward(from: 0);
    return RepaintBoundary(
      child: CustomPaint(
        painter: _CartesianAxesPainter(
          stateProperties: widget.stateProperties,
          axisAnimation: axisAnimation,
          renderType: widget.renderType,
          isRepaint: widget.stateProperties.chartAxis.needsRepaint,
          notifier: axisRepaintNotifier,
        ),
      ),
    );
  }

  void _animateAxis() {
    final double animationFactor = animationController.value;
    for (int axisIndex = 0; axisIndex < widget.stateProperties.chartAxis.axisRenderersCollection.length; axisIndex++) {
      final ChartAxisRenderer axisRenderer = widget.stateProperties.chartAxis.axisRenderersCollection[axisIndex];
      final dynamic axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);

      dynamic oldAxisRenderer;
      bool needAnimate = false;
      if ((widget.stateProperties.requireInvertedAxis
              ? axisDetails.orientation == AxisOrientation.vertical
              : axisDetails.orientation == AxisOrientation.horizontal) &&
          widget.stateProperties.oldAxisRenderers.isNotEmpty &&
          (axisDetails.axis.visibleMinimum != null || axisDetails.axis.visibleMaximum != null)) {
        oldAxisRenderer = getOldAxisRenderer(axisRenderer, widget.stateProperties.oldAxisRenderers);
        final dynamic oldAxisDetails = AxisHelper.getAxisRendererDetails(oldAxisRenderer);
        if (oldAxisRenderer != null &&
            (oldAxisDetails.axis.visibleMinimum != null && oldAxisDetails.axis.visibleMaximum != null)) {
          needAnimate = axisDetails.runtimeType == oldAxisDetails.runtimeType &&
              ((oldAxisDetails.axis.visibleMinimum != null &&
                      oldAxisDetails.axis.visibleMinimum != axisDetails.axis.visibleMinimum) ||
                  (oldAxisDetails.axis.visibleMaximum != null &&
                      oldAxisDetails.axis.visibleMaximum != axisDetails.axis.visibleMaximum)) &&
              _checkSeriesAnimation(axisDetails.seriesRenderers);
          if (needAnimate) {
            if (axisRenderer is DateTimeAxisRenderer || axisRenderer is DateTimeCategoryAxisRenderer) {
              axisDetails.visibleMinimum = (oldAxisDetails.axis.visibleMinimum.millisecondsSinceEpoch -
                      (oldAxisDetails.axis.visibleMinimum.millisecondsSinceEpoch -
                              axisDetails.axis.visibleMinimum.millisecondsSinceEpoch) *
                          animationFactor)
                  .toInt();
              axisDetails.visibleMaximum = (oldAxisDetails.axis.visibleMaximum.millisecondsSinceEpoch -
                      (oldAxisDetails.axis.visibleMaximum.millisecondsSinceEpoch -
                              axisDetails.axis.visibleMaximum.millisecondsSinceEpoch) *
                          animationFactor)
                  .toInt();
            } else {
              axisDetails.visibleMinimum = oldAxisDetails.axis.visibleMinimum -
                  (oldAxisDetails.axis.visibleMinimum - axisDetails.axis.visibleMinimum) * animationFactor;
              axisDetails.visibleMaximum = oldAxisDetails.axis.visibleMaximum -
                  (oldAxisDetails.axis.visibleMaximum - axisDetails.axis.visibleMaximum) * animationFactor;
            }
            if (axisDetails is DateTimeCategoryAxisDetails) {
              axisDetails.labels.clear();
              for (final CartesianSeriesRenderer seriesRenderer in axisDetails.seriesRenderers) {
                widget.stateProperties.chartSeries
                    .findSeriesMinMax(SeriesHelper.getSeriesRendererDetails(seriesRenderer));
              }
            }
            axisDetails.calculateRangeAndInterval(widget.stateProperties);
            for (final CartesianSeriesRenderer seriesRenderer in axisDetails.seriesRenderers) {
              final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
              seriesRendererDetails.calculateRegion = true;
              seriesRendererDetails.repaintNotifier.value++;
              widget.stateProperties.plotBandRepaintNotifier.value++;
              if (seriesRendererDetails.series.dataLabelSettings.isVisible == true &&
                  widget.stateProperties.renderDataLabel?.state != null) {
                widget.stateProperties.renderDataLabel?.state!.dataLabelRepaintNotifier.value++;
              }
            }
          }
        }
      }
    }
  }

  bool _checkSeriesAnimation(List<CartesianSeriesRenderer> seriesRenderers) {
    for (int i = 0; i < seriesRenderers.length; i++) {
      final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderers[i]);
      if (seriesRendererDetails.series.animationDuration <= 0) {
        return false;
      }
    }
    return true;
  }

  @override
  void dispose() {
    disposeAnimationController(animationController, _repaintAxisElements);
    super.dispose();
  }

  void _repaintAxisElements() {
    _animateAxis();
    axisRepaintNotifier.value++;
    if (animationController.status == AnimationStatus.completed) {
      widget.stateProperties.renderingDetails.dataLabelTemplateRegions.clear();
      widget.dataLabelTemplateNotifier.value++;
    }
  }
}

class _CartesianAxesPainter extends CustomPainter {
  _CartesianAxesPainter(
      {required this.stateProperties,
      required this.isRepaint,
      required ValueNotifier<num> notifier,
      required this.renderType,
      required this.axisAnimation,})
      : chart = stateProperties.chart,
        super(repaint: notifier);
  final CartesianStateProperties stateProperties;
  final CgCartesianChart chart;
  final bool isRepaint;
  final String renderType;
  final Animation<double> axisAnimation;

  @override
  void paint(Canvas canvas, Size size) {
    _onAxisDraw(canvas);
  }

  void _onAxisDraw(Canvas canvas) {
    if (renderType == 'outside') {
      _drawPlotAreaBorder(canvas);
      if (chart.plotAreaBackgroundImage != null && stateProperties.backgroundImage != null) {
        paintImage(
            canvas: canvas,
            rect: stateProperties.chartAxis.axisClipRect,
            image: stateProperties.backgroundImage!,
            fit: BoxFit.fill,);
      }
    }
    _drawAxes(canvas);
  }

  void _drawPlotAreaBorder(Canvas canvas) {
    final Rect axisClipRect = stateProperties.chartAxis.axisClipRect;
    final RenderingDetails renderingDetails = stateProperties.renderingDetails;
    final Paint paint = Paint();
    paint.color = chart.plotAreaBorderColor ?? renderingDetails.chartTheme.plotAreaBorderColor;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = chart.plotAreaBorderWidth;
    chart.plotAreaBorderWidth == 0 ? paint.color = Colors.transparent : paint.color = paint.color;
    canvas.drawRect(axisClipRect, paint);

    canvas.drawRect(
        axisClipRect,
        Paint()
          ..color = chart.plotAreaBackgroundColor ?? renderingDetails.chartTheme.plotAreaBackgroundColor
          ..style = PaintingStyle.fill,);
  }

  void _drawHorizontalAxes(Canvas canvas, ChartAxisRenderer axisRenderer, double animationFactor,
      ChartAxisRenderer? oldAxisRenderer, bool? needAnimate,) {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    final ChartAxis axis = axisDetails.axis;
    if (axis.isVisible) {
      if (axis.axisLine.width > 0 && renderType == 'outside') {
        axisRenderer.drawHorizontalAxesLine(canvas, axisRenderer, chart);
      }
      if (axis.majorTickLines.width > 0 || axis.majorGridLines.width > 0) {
        axisRenderer.drawHorizontalAxesTickLines(
            canvas, axisRenderer, chart, renderType, animationFactor, oldAxisRenderer, needAnimate,);
      }
      axisRenderer.drawHorizontalAxesLabels(
          canvas, axisRenderer, chart, renderType, animationFactor, oldAxisRenderer, needAnimate,);
      if (axisDetails.isMultiLevelLabelEnabled) {
        drawMultiLevelLabels(axisDetails, canvas);
      }
      if (renderType == 'outside') {
        axisRenderer.drawHorizontalAxesTitle(canvas, axisRenderer, chart);
      }
    }
  }

  void _drawVerticalAxes(Canvas canvas, ChartAxisRenderer axisRenderer, double animationFactor,
      ChartAxisRenderer? oldAxisRenderer, bool? needAnimate,) {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
    final ChartAxis axis = axisDetails.axis;
    if (axis.isVisible) {
      if (axis.axisLine.width > 0 && renderType == 'outside') {
        axisRenderer.drawVerticalAxesLine(canvas, axisRenderer, chart);
      }
      if (axisDetails.visibleLabels.isNotEmpty && (axis.majorTickLines.width > 0 || axis.majorGridLines.width > 0)) {
        axisRenderer.drawVerticalAxesTickLines(
            canvas, axisRenderer, chart, renderType, animationFactor, oldAxisRenderer, needAnimate,);
      }
      axisRenderer.drawVerticalAxesLabels(
          canvas, axisRenderer, chart, renderType, animationFactor, oldAxisRenderer, needAnimate,);
      if (axisDetails.isMultiLevelLabelEnabled) {
        drawMultiLevelLabels(axisDetails, canvas);
      }
      if (renderType == 'outside') {
        axisRenderer.drawVerticalAxesTitle(canvas, axisRenderer, chart);
      }
    }
  }

  void _drawAxes(Canvas canvas) {
    final double animationFactor = axisAnimation.value;
    for (int axisIndex = 0; axisIndex < stateProperties.chartAxis.axisRenderersCollection.length; axisIndex++) {
      final ChartAxisRenderer axisRenderer = stateProperties.chartAxis.axisRenderersCollection[axisIndex];
      final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
      final ChartAxis axis = axisDetails.axis;
      axisDetails.isInsideTickPosition = axis.tickPosition == TickPosition.inside;
      ChartAxisRenderer? oldAxisRenderer;
      bool needAnimate = false;
      if (stateProperties.oldAxisRenderers.isNotEmpty && axisDetails.visibleRange != null) {
        oldAxisRenderer = getOldAxisRenderer(axisRenderer, stateProperties.oldAxisRenderers);
        if (oldAxisRenderer != null) {
          final ChartAxisRendererDetails oldAxisDetails = AxisHelper.getAxisRendererDetails(oldAxisRenderer);
          if (oldAxisDetails.visibleRange != null) {
            needAnimate = chart.enableAxisAnimation &&
                (oldAxisDetails.visibleRange!.minimum != axisDetails.visibleRange!.minimum ||
                    oldAxisDetails.visibleRange!.maximum != axisDetails.visibleRange!.maximum);
          }
        }
      }
      axisDetails.orientation == AxisOrientation.horizontal
          ? _drawHorizontalAxes(canvas, axisRenderer, animationFactor, oldAxisRenderer, needAnimate)
          : _drawVerticalAxes(canvas, axisRenderer, animationFactor, oldAxisRenderer, needAnimate);
    }
  }

  @override
  bool shouldRepaint(_CartesianAxesPainter oldDelegate) => isRepaint;
}
